#include <CL/cl.h>
#include <stdio.h>
#include <stdlib.h>
#include <wtypes.h>
#include <Strsafe.h>
#include <string.h>
#include <string>
#include <vector>
#include <omp.h>
#include <cstdlib>
#include <fstream>
#include <time.h>	
#include <math.h>
#include <QApplication>
#include <QTextStream>
#include <QFile>
#include "SLAE.h"
#include "WriteLog.h"

SLAE::SLAE(cl_uint sizeVector, float precision)
{
	size = sizeVector;
	solvePrecision = precision;
	initializeHost();
	initializeCL();
}

SLAE::~SLAE()
{
	cleanupCL();
	cleanupHost();
}

void SLAE::initializeHost()
{
	pMatrix			= NULL;
	pVector			= NULL;
	pResult			= NULL;
	pNextSolution	= NULL;

	pMatrix = (cl_float*) malloc (size * size * sizeof(cl_float));
	if(pMatrix == NULL)
	{
		WriteLog::writeToLog("Failed to allocate pMatrix memory on host", "Error: ", "SLAE");
		return;
	}

	pVector = (cl_float*) malloc (size * sizeof(cl_float));
	if(pVector == NULL)
	{
		WriteLog::writeToLog("Failed to allocate pVector memory on host", "Error: ", "SLAE");
		return;
	}

	pResult = (cl_float*) malloc (size * sizeof(cl_float));
	if(pResult == NULL)
	{
		WriteLog::writeToLog("Failed to allocate pResult memory on host", "Error: ", "SLAE");
		return;
	}

	pNextSolution = (cl_float*) malloc (size * sizeof(cl_float));
	if(pNextSolution == NULL)
	{
		WriteLog::writeToLog("Failed to allocate pNextSolution memory on host", "Error: ", "SLAE");
		return;
	}
	// Initial values
	unsigned int i=0, j=0;
	for(i=0; i<size; i++)
	{
		pVector[i] = i+1;
		pResult[i] = 0;
		pNextSolution[i] = -1;
		for(j=0; j<size; j++)
		{
			if(j <= i)
				pMatrix[i*size + j] = 1;
			else
				pMatrix[i*size + j] = 0;
			
		}
	}


}

// OpenCL related initializations are done here.
// Context, Device list, Command Queue are set up.
// Calls are made to set up OpenCL memory buffers that this program uses
// and to load the programs into memory and get kernel handles.
void SLAE::initializeCL(void)
{
	cl_int status = 0;
    size_t deviceListSize;

	// Create OpenCL memory buffers
	pMatrixBuffer = clCreateBuffer(
						context, 
						CL_MEM_READ_WRITE | CL_MEM_USE_HOST_PTR,
						sizeof(cl_float) * size * size,
						pMatrix, 
						&status);
	if(status != CL_SUCCESS) 
	{ 
		writeErrorToLog(status, "Can't create buffer 'pMatrixBuffer' (clCreateBuffer)", "SLAE");
		return;
	}
	// Create OpenCL memory buffers
    pVectorBuffer = clCreateBuffer(
					   context, 
                       CL_MEM_READ_WRITE | CL_MEM_USE_HOST_PTR,
                       sizeof(cl_float) * size,
                       pVector, 
                       &status);
    if(status != CL_SUCCESS) 
	{ 
		writeErrorToLog(status, "Can't create buffer 'pVectorBuffer' (clCreateBuffer)", "SLAE");
		return;
	}
	// Create OpenCL memory buffers
    pResultBuffer = clCreateBuffer(
					   context, 
                       CL_MEM_READ_WRITE | CL_MEM_USE_HOST_PTR,
                       sizeof(cl_float) * size,
                       pResult, 
                       &status);
    if(status != CL_SUCCESS) 
	{ 
		writeErrorToLog(status, "Can't create buffer 'pResultBuffer' (clCreateBuffer)", "SLAE");
		return;
	}

	// Create OpenCL memory buffers
    pNextSolutionBuffer = clCreateBuffer(
					   context, 
                       CL_MEM_READ_WRITE | CL_MEM_USE_HOST_PTR,
                       sizeof(cl_float) * size,
                       pNextSolution,
                       &status);
    if(status != CL_SUCCESS) 
	{ 
		writeErrorToLog(status, "Can't create buffer 'pNextSolutionBuffer' (clCreateBuffer)", "SLAE");
		return;
	}
	
	// Load CL file, build CL program object, create CL kernel object
    const char * filename  = "SLAE.cl";
    string  sourceStr = convertToString(filename);
    const char * source    = sourceStr.c_str();
    size_t sourceSize[]    = { strlen(source) };

    program = clCreateProgramWithSource(
			      context, 
                  1, 
                  &source,
				  sourceSize,
                  &status);
	if(status != CL_SUCCESS) 
	{ 
		writeErrorToLog(status, "Loading Binary into cl_program (clCreateProgramWithBinary)", "SLAE");
		return;
	}

    // create a cl program executable for all the devices specified 
    status = clBuildProgram(program, 1, devices, NULL, NULL, NULL);
	if(status != CL_SUCCESS) 
	{ 
		writeErrorToLog(status, "Building Program (clBuildProgram)", "SLAE");
		return; 
	}

    // get a kernel object handle for a kernel with the given name 
    kernel = clCreateKernel(program, "Main", &status);
    if(status != CL_SUCCESS) 
	{  
		writeErrorToLog(status, "Creating Kernel from program (clCreateKernel)", "SLAE");
		return;
	}
}

// this is called once the OpenCL context, memory etc. are set up,
// the program is loaded into memory and the kernel handles are ready.
// It sets the values for kernels' arguments and enqueues calls to the kernels
// on to the command queue and waits till the calls have finished execution.
// It also gets kernel start and end time if profiling is enabled.
void SLAE::runCLKernels(void)
{
	cl_int   status;
    cl_event events[2];
    size_t globalThreads[1];
    size_t localThreads[1];

    
    globalThreads[0] = size;
	localThreads[0] = 1;

    // === Set appropriate arguments to the kernel ===
	// the matrix to the kernel (input)
	status = clSetKernelArg(
					kernel, 
					0,				
					sizeof(cl_mem), 
					(void *)&pMatrixBuffer);
	if(status != CL_SUCCESS) 
	{ 
		writeErrorToLog(status, "Setting kernel argument 'pMatrixBuffer' (clSetKernelArg)", "SLAE");
		return;
	}
    // the  vector to the kernel (input)
    status = clSetKernelArg(
                    kernel, 
                    1,
                    sizeof(cl_mem), 
                    (void *)&pVectorBuffer);
    if(status != CL_SUCCESS) 
	{ 
		writeErrorToLog(status, "Setting kernel argument 'pVectorBuffer' (clSetKernelArg)", "SLAE");
		return;
	}

	// the result to the kernel (output)
    status = clSetKernelArg(
                    kernel, 
                    2,
                    sizeof(cl_mem), 
                    (void *)&pResultBuffer);
    if(status != CL_SUCCESS) 
	{ 
		writeErrorToLog(status, "Setting kernel argument 'pResultBuffer' (clSetKernelArg)", "SLAE");
		return;
	}

	// size of vector to the kernel (input)
	status = clSetKernelArg(
					kernel, 
					3,
					sizeof(cl_uint), 
					(void *)&size);
	if(status != CL_SUCCESS) 
	{ 
		writeErrorToLog(status, "Setting kernel argument 'size' (clSetKernelArg)", "SLAE");
		return;
	}

	// solve precision to the kernel (input)
	status = clSetKernelArg(
					kernel, 
					4,
					sizeof(cl_float), 
					(void *)&solvePrecision);
	if(status != CL_SUCCESS) 
	{ 
		writeErrorToLog(status, "Setting kernel argument 'solvePrecision' (clSetKernelArg)", "SLAE");
		return;
	}

	// the matrix to the kernel (input)
	status = clSetKernelArg(
					kernel, 
					5,				
					sizeof(cl_mem), 
					(void *)&pNextSolutionBuffer);
	if(status != CL_SUCCESS) 
	{ 
		writeErrorToLog(status, "Setting kernel argument 'pNextSolutionBuffer' (clSetKernelArg)", "SLAE");
		return;
	}

	startGPU = omp_get_wtime();	// get start time

	// Enqueue a kernel run call.
    status = clEnqueueNDRangeKernel(
			     commandQueue,
                 kernel,
                 1,
                 NULL,
                 globalThreads,
                 localThreads,
                 0,
                 NULL,
                 &events[0]);

	endGPU = omp_get_wtime();	// get end time

    if(status != CL_SUCCESS) 
	{ 
		writeErrorToLog(status, "Enqueueing kernel onto command queue (clEnqueueNDRangeKernel)", "SLAE");
		return;
	}

	clFinish(commandQueue);

    // wait for the kernel call to finish execution
    status = clWaitForEvents(1, &events[0]);
    if(status != CL_SUCCESS) 
	{ 
		writeErrorToLog(status, "Waiting for kernel run to finish (clWaitForEvents)", "SLAE");
		return;
	}

    status = clReleaseEvent(events[0]);
    if(status != CL_SUCCESS)
    {
        writeErrorToLog(status, "Can't release event 'events[0]' (clReleaseEvent)", "SLAE");
        return;
    }

    // Enqueue readBuffer
    status = clEnqueueReadBuffer(
                commandQueue,
                pResultBuffer,
                CL_TRUE,
                0,
                size * sizeof(cl_float),
                pResult,
                0,
                NULL,
                &events[1]);
    
    if(status != CL_SUCCESS) 
	{
		writeErrorToLog(status, "Can't read from buffer 'outputBuffer' (clEnqueueReadBuffer)", "SLAE");
    }

	// Wait for the read buffer to finish execution
    status = clWaitForEvents(1, &events[1]);
    if(status != CL_SUCCESS) 
	{ 
		writeErrorToLog(status, "Waiting for read buffer call to finish (clWaitForEvents)", "SLAE");
		return;
	}
    
    status = clReleaseEvent(events[1]);
    if(status != CL_SUCCESS)
    {
		writeErrorToLog(status, "Can't release event 'events[1]' (clReleaseEvent)", "SLAE");
        return;
    }
}
void SLAE::cleanupHost(void)
{
	if(pMatrix != NULL)
	{
		free(pMatrix);
		pMatrix = NULL;
	}

	if(pVector != NULL)
	{
		free(pVector);
		pVector = NULL;
	}
	
	if(pResult != NULL)
	{
		free(pResult);
		pResult = NULL;
	}

	if(pResultCPU != NULL)
	{
		delete [] pResultCPU;
		pResultCPU = NULL;
	}
}
void SLAE::cleanupCL(void)
{
	cl_int status;

    status = clReleaseKernel(kernel);
    if(status != CL_SUCCESS)
	{
		writeErrorToLog(status, "Can't release kernel 'kernel' (clReleaseKernel)", "SLAE");
		return; 
	}
    status = clReleaseProgram(program);
    if(status != CL_SUCCESS)
	{
		writeErrorToLog(status, "Can't release program 'program' (clReleaseProgram)", "SLAE");
		return; 
	}
    
	status = clReleaseMemObject(pMatrixBuffer);
    if(status != CL_SUCCESS)
	{
		writeErrorToLog(status, "Can't release memmory object 'pMatrixBuffer' (clReleaseMemObject)", "SLAE");
		return; 
	}

	status = clReleaseMemObject(pVectorBuffer);
    if(status != CL_SUCCESS)
	{
		writeErrorToLog(status, "Can't release memmory object 'pVectorBuffer' (clReleaseMemObject)", "SLAE");
		return; 
	}

	status = clReleaseMemObject(pResultBuffer);
    if(status != CL_SUCCESS)
	{
		writeErrorToLog(status, "Can't release memmory object 'pResultBuffer' (clReleaseMemObject)", "SLAE");
		return; 
	}

	MyOCL::cleanupCL();
}


void SLAE::printTest()
{
	/*write to file CPU result*/
	QString namefile = "Result_SLAE.txt";
	QString path = QApplication::applicationDirPath();
	QFile file(path + "/" + namefile);
	if(!file.open(QIODevice::WriteOnly))
	{
		return;
	}

	QTextStream stream(&file);
	stream << "CPU: \n";
	for(int i=0; i<size; i++)
	{
		stream << pResultCPU[i];
		stream << " ";
	}
	stream << "\n";

	stream << "GPU: \n";
	for(int i=0; i<size; i++)
	{
		stream << pResult[i];
		stream << " ";
	}
	stream << "\n";

	file.close();
}

// size			- dimension of the matrix (size*size)
// pMatrix		- coefficient matrix					
// pVector		- column of free terms				
// pResultCPU	- first approximation				
void SLAE::Jacobi()
{
	int iter = 0;
	long float * pTempX = new long float[size];
	pResultCPU = new long float[size];
	for(unsigned int i=0; i<size; i++)
	{
		pResultCPU[i] = 0;
	}

	long float norm = 0.0f;		// norm, defined as the greatest difference between the components of the column 'X' adjacent iterations.
	float eps = solvePrecision;;	//prescribed accuracy
	startCPU = omp_get_wtime();
	do
	{
#pragma omp parallel for
			for (int i = 0; i < size; i++) 
			{
				pTempX[i] = - pVector[i];
	#pragma omp parallel for
				for (int g = 0; g < size; g++) 
				{
					if (i != g)
						pTempX[i] += pMatrix[i*size + g] * pResultCPU[g];
				}
				pTempX[i] /= - pMatrix[i*size + i];
			}
        norm = fabs(pResultCPU[0] - pTempX[0]);

	#pragma omp parallel for
		for (int h = 0; h < size; h++) 
		{
			if (fabs(pResultCPU[h] - pTempX[h]) > norm)
				norm = fabs(pResultCPU[h] - pTempX[h]);
			pResultCPU[h] = pTempX[h];
		}

		iter++;
	}
	while(norm > eps);
	endCPU = omp_get_wtime();

	// CPU result
	/*
	for(unsigned int i=0; i<size; i++)
	{
		cout << pResultCPU[i] << " ";
	}
	*/

	delete [] pTempX;
}